Skip to content

Custom Events

Creating your own event types with automatic serialization

HPD-Agent lets you create custom events by simply extending AgentEvent. The source generator automatically handles serialization, registration, and Native AOT support.

Quick Start

Creating a custom event is as simple as defining a record:

csharp
// That's it! Source generator handles everything else
public record AnalysisProgressEvent(
    string Stage,
    int PercentComplete
) : AgentEvent;

The source generator automatically:

  1. Creates EventTypes.Custom.ANALYSIS_PROGRESS constant
  2. Registers the event with AgentEventSerializer
  3. Adds [JsonSerializable] attributes for Native AOT

Basic Custom Events

Simple Progress Event

csharp
public record AnalysisProgressEvent(
    string Stage,
    int PercentComplete
) : AgentEvent;

// Usage in middleware
coordinator.Emit(new AnalysisProgressEvent("Loading data", 25));

// Consuming
await foreach (var evt in agent.RunAsync(messages))
{
    switch (evt)
    {
        case AnalysisProgressEvent progress:
            UpdateProgressBar(progress.PercentComplete);
            Console.WriteLine($"{progress.Stage}: {progress.PercentComplete}%");
            break;
    }
}

Custom Notification Event

csharp
public record WorkflowStepCompletedEvent(
    string StepName,
    bool Success,
    string? ErrorMessage = null,
    TimeSpan Duration = default
) : AgentEvent;

// Usage
coordinator.Emit(new WorkflowStepCompletedEvent(
    "Data validation",
    Success: true,
    Duration: TimeSpan.FromSeconds(2.5)
));

Custom Event Type Names

By default, the event type name is auto-generated from the class name:

csharp
AnalysisProgressEvent → "ANALYSIS_PROGRESS"
WorkflowStepCompletedEvent → "WORKFLOW_STEP_COMPLETED"
MyCustomEvent → "MY_CUSTOM"

Override with [EventType] Attribute

csharp
[EventType("CUSTOM_WORKFLOW_STEP")]
public record WorkflowStepEvent(
    string StepName,
    bool Success
) : AgentEvent;

// Serialized as: {"type": "CUSTOM_WORKFLOW_STEP", ...}

What the Source Generator Provides

1. EventTypes Constants

csharp
// Generated: CustomEventTypes.g.cs
public static partial class EventTypes
{
    public static class Custom
    {
        public const string ANALYSIS_PROGRESS = "ANALYSIS_PROGRESS";
        public const string WORKFLOW_STEP_COMPLETED = "WORKFLOW_STEP_COMPLETED";
        public const string CUSTOM_WORKFLOW_STEP = "CUSTOM_WORKFLOW_STEP";
    }
}

Usage:

csharp
if (eventType == EventTypes.Custom.ANALYSIS_PROGRESS)
{
    // Handle analysis progress
}

2. Automatic Serialization

csharp
var evt = new AnalysisProgressEvent("Processing", 50);
var json = AgentEventSerializer.ToJson(evt);

// Output:
// {
//   "version": "1.0",
//   "type": "ANALYSIS_PROGRESS",
//   "stage": "Processing",
//   "percentComplete": 50
// }

3. Automatic Deserialization

csharp
var json = """
{
  "version": "1.0",
  "type": "ANALYSIS_PROGRESS",
  "stage": "Loading",
  "percentComplete": 75
}
""";

var evt = AgentEventSerializer.FromJson<AnalysisProgressEvent>(json);
Console.WriteLine($"{evt.Stage}: {evt.PercentComplete}%");

Advanced Patterns

Custom Bidirectional Events

For request/response patterns:

csharp
public interface ICustomBidirectionalEvent : IBidirectionalEvent
{
    string CustomRequestId { get; }
}

public record DataValidationRequestEvent(
    string CustomRequestId,
    string SourceName,
    string DataToValidate
) : AgentEvent, ICustomBidirectionalEvent;

public record DataValidationResponseEvent(
    string CustomRequestId,
    string SourceName,
    bool IsValid,
    string? ValidationMessage = null
) : AgentEvent, ICustomBidirectionalEvent;

Usage in middleware:

csharp
var requestId = Guid.NewGuid().ToString();

coordinator.Emit(new DataValidationRequestEvent
{
    CustomRequestId = requestId,
    SourceName = "ValidationMiddleware",
    DataToValidate = inputData
});

var response = await coordinator.WaitForResponseAsync<DataValidationResponseEvent>(
    requestId,
    timeout: TimeSpan.FromSeconds(30),
    cancellationToken: ct);

if (!response.IsValid)
{
    throw new ValidationException(response.ValidationMessage);
}

Custom Observability Events

For internal diagnostics:

csharp
public record PerformanceMetricEvent(
    string OperationName,
    TimeSpan Duration,
    long MemoryUsed
) : AgentEvent, IObservabilityEvent;

// This will be automatically filtered by:
// if (evt is IObservabilityEvent) continue;

Custom Priority Events

For urgent events:

csharp
public record EmergencyStopEvent(
    string Reason
) : AgentEvent
{
    public EmergencyStopEvent(string Reason) : this()
    {
        this.Reason = Reason;
        Priority = EventPriority.Immediate;  // Bypass queued events
    }
}

Event Properties

All custom events inherit these from AgentEvent:

csharp
public record MyCustomEvent(string Data) : AgentEvent
{
    public MyCustomEvent(string Data) : this()
    {
        this.Data = Data;

        // Optional: Set properties
        Priority = EventPriority.Control;
        CanInterrupt = false;  // Don't drop on cancellation
        Direction = EventDirection.Upstream;
        StreamId = "my-stream";
    }
}

Available Properties

  • ExecutionContext - Agent that emitted this event
  • Priority - Immediate, Control, Normal, Background
  • SequenceNumber - Auto-assigned by coordinator
  • Direction - Downstream or Upstream
  • StreamId - For grouping related events
  • CanInterrupt - Can be dropped on stream interruption

Emitting Custom Events

From Middleware

csharp
public class AnalysisMiddleware : IAgentMiddleware
{
    public async Task<ModelResponse> WrapModelCallAsync(
        ModelRequest request,
        IAgentContext context,
        Func<ModelRequest, Task<ModelResponse>> next)
    {
        var coordinator = context.EventCoordinator;

        // Emit custom progress events
        coordinator.Emit(new AnalysisProgressEvent("Analyzing", 0));

        // Do work...
        await Task.Delay(1000);

        coordinator.Emit(new AnalysisProgressEvent("Processing", 50));

        var response = await next(request);

        coordinator.Emit(new AnalysisProgressEvent("Complete", 100));

        return response;
    }
}

From Tools/Toolkits

csharp
public class MyTools
{
    private readonly IEventCoordinator _coordinator;

    public MyTools(IEventCoordinator coordinator)
    {
        _coordinator = coordinator;
    }

    [AIFunction]
    public async Task<string> ProcessData(string data)
    {
        _coordinator.Emit(new AnalysisProgressEvent("Starting", 0));

        // Process...
        await Task.Delay(1000);

        _coordinator.Emit(new AnalysisProgressEvent("Done", 100));

        return "Processed";
    }
}

Consuming Custom Events

Same pattern as built-in events:

csharp
await foreach (var evt in agent.RunAsync(messages))
{
    if (evt is IObservabilityEvent) continue;

    switch (evt)
    {
        // Built-in events
        case TextDeltaEvent delta:
            Console.Write(delta.Text);
            break;

        // Custom events
        case AnalysisProgressEvent progress:
            UpdateProgressBar(progress.PercentComplete);
            break;

        case WorkflowStepCompletedEvent step:
            if (step.Success)
                Console.WriteLine($"✓ {step.StepName}");
            else
                Console.WriteLine($"✗ {step.StepName}: {step.ErrorMessage}");
            break;
    }
}

Source Generator Diagnostics

The source generator provides helpful diagnostics:

CodeDescriptionFix
HPD010Duplicate event type discriminatorUse [EventType("UNIQUE_NAME")] to resolve
HPD011Generic events not supportedRemove generic type parameters
HPD012Abstract events skipped (info only)Concrete events will be generated

Example: HPD010

csharp
//   ERROR: Both generate "MY_EVENT" discriminator
public record MyEvent() : AgentEvent;
public record MyEvent2() : AgentEvent;  // Naming collision!

//   FIX: Use explicit discriminators
[EventType("MY_EVENT_V1")]
public record MyEvent() : AgentEvent;

[EventType("MY_EVENT_V2")]
public record MyEvent2() : AgentEvent;

Native AOT Support

The source generator automatically adds [JsonSerializable] attributes for Native AOT:

csharp
// Generated: CustomEventJsonContext.g.cs
[JsonSerializable(typeof(AnalysisProgressEvent))]
[JsonSerializable(typeof(WorkflowStepCompletedEvent))]
// ... etc
partial class CustomEventJsonContext : JsonSerializerContext
{
}

No manual registration needed!

Best Practices

Use Records

csharp
// GOOD: Records are immutable and work well with pattern matching
public record MyEvent(string Data) : AgentEvent;

Name Events Descriptively

csharp
// GOOD: Clear what this event represents
public record DocumentProcessingCompletedEvent(...)
public record ValidationFailedEvent(...)
public record AnalysisProgressEvent(...)

// BAD: Vague names
public record Event1(...)
public record CustomEvent(...)

Include Relevant Data

csharp
// GOOD: All context needed to handle the event
public record ProcessingErrorEvent(
    string OperationName,
    string ErrorMessage,
    Exception Exception,
    int RetryCount
) : AgentEvent;

// BAD: Missing context
public record ErrorEvent(string Message) : AgentEvent;

Use Marker Interfaces

csharp
// GOOD: Categorize related events
public interface IWorkflowEvent
{
    string WorkflowId { get; }
}

public record WorkflowStartedEvent(
    string WorkflowId,
    string WorkflowName
) : AgentEvent, IWorkflowEvent;

public record WorkflowCompletedEvent(
    string WorkflowId,
    bool Success
) : AgentEvent, IWorkflowEvent;

// Filter by interface
if (evt is IWorkflowEvent workflowEvt)
{
    TrackWorkflow(workflowEvt.WorkflowId);
}

Don't Use Generic Events

csharp
//   NOT SUPPORTED: Generic events don't work
public record GenericEvent<T>(T Data) : AgentEvent;

//   USE: Concrete types instead
public record StringDataEvent(string Data) : AgentEvent;
public record IntDataEvent(int Data) : AgentEvent;

Complete Example

csharp
// Define custom events
public record AnalysisStartedEvent(
    string AnalysisId,
    string DataSource
) : AgentEvent;

public record AnalysisProgressEvent(
    string AnalysisId,
    string Stage,
    int PercentComplete
) : AgentEvent;

public record AnalysisCompletedEvent(
    string AnalysisId,
    string Result,
    TimeSpan Duration
) : AgentEvent;

// Use in middleware
public class AnalysisMiddleware : IAgentMiddleware
{
    public async Task<ModelResponse> WrapModelCallAsync(
        ModelRequest request,
        IAgentContext context,
        Func<ModelRequest, Task<ModelResponse>> next)
    {
        var analysisId = Guid.NewGuid().ToString();
        var coordinator = context.EventCoordinator;

        coordinator.Emit(new AnalysisStartedEvent(analysisId, "UserInput"));

        coordinator.Emit(new AnalysisProgressEvent(analysisId, "Validation", 25));
        // ... validate

        coordinator.Emit(new AnalysisProgressEvent(analysisId, "Processing", 50));
        var response = await next(request);

        coordinator.Emit(new AnalysisProgressEvent(analysisId, "Finalizing", 75));
        // ... finalize

        coordinator.Emit(new AnalysisCompletedEvent(
            analysisId,
            "Success",
            TimeSpan.FromSeconds(3)));

        return response;
    }
}

// Consume in UI
await foreach (var evt in agent.RunAsync(messages))
{
    switch (evt)
    {
        case AnalysisStartedEvent started:
            Console.WriteLine($"Analysis {started.AnalysisId} started");
            break;

        case AnalysisProgressEvent progress:
            UpdateProgressBar(progress.PercentComplete);
            Console.WriteLine($"{progress.Stage}: {progress.PercentComplete}%");
            break;

        case AnalysisCompletedEvent completed:
            Console.WriteLine($"Completed in {completed.Duration.TotalSeconds}s");
            break;
    }
}

See Also

Released under the MIT License.